SPB Git forge

spb/countryatlas

Public
20commits 1branches 0releases
268.3 MBsize
maindefault branch
12 days agolast push
TypeScript 57% Python 38.6% JavaScript 3.6% CSS 0.6%
8.1 KB · 151 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { notFound } from 'next/navigation';4import { t, tOpt } from '@/i18n';5import { api, isNotBuilt, isNotFound, safe } from '@/lib/api';6import { apiAnalytics } from '@/lib/api-analytics';7import { apiExplore } from '@/lib/api-explore';8import { fixed, formatValue, grouped } from '@/lib/format';9import { seoTitle } from '@/lib/seo';10import { routes } from '@/lib/site';11import type { IndicatorCard } from '@/lib/types';12import type { RegionResponse } from '@/lib/types-explore';13import { NotBuiltState } from '@/components/data/empty-state';14import { Section } from '@/components/data/section';15import { ACTION_CLS, PageHeader } from '@/components/explore/page-header';16import { GroupComparePicker } from '@/components/regions/group-compare-picker';17import { GroupHistory } from '@/components/regions/group-history';18import { MemberMap } from '@/components/regions/member-map';19import { MemberRanking } from '@/components/regions/member-ranking';20import { MembersTable } from '@/components/regions/members-table';2122export const revalidate = 900;2324type Params = { slug: string };2526async function load(slug: string): Promise<RegionResponse | 'not-built' | null> {27  try {28    return await apiExplore.region(slug);29  } catch (e) {30    if (isNotFound(e)) return null;31    if (isNotBuilt(e)) return 'not-built';32    throw e;33  }34}3536export async function generateMetadata({ params }: { params: Promise<Params> }): Promise<Metadata> {37  const { slug } = await params;38  const data = await load(slug);39  if (!data || data === 'not-built') return { title: t('region.notFound'), robots: { index: false } };40  const name = data.group.name ?? slug;41  const title = seoTitle.region(name);42  const description = t('region.description', { name, n: data.n_members });43  const canonical = routes.region(data.group.slug ?? slug);44  return { title: { absolute: `${title} | ${t('site.name')}` }, description, alternates: { canonical }, openGraph: { title: `${title} | ${t('site.name')}`, description, url: canonical, type: 'article' }, twitter: { card: 'summary_large_image', title, description } };45}4647/** Extra indicators offered in the member ranking picker besides the headline ones. */48const EXTRA_RANKING = ['gdp-per-capita-ppp', 'population-growth', 'fertility-rate', 'renewable-electricity-share', 'internet-users', 'co2-per-capita', 'general-government-gross-debt-pct-gdp', 'median-age'];4950export default async function RegionPage({ params }: { params: Promise<Params> }) {51  const { slug } = await params;52  const data = await load(slug);53  if (data === null) notFound();54  if (data === 'not-built') return <NotBuiltState />;5556  const g = data.group;57  const name = g.name ?? slug;58  const gslug = g.slug ?? g.id;59  const kindLabel = tOpt(`regions.kindLabel.${g.kind ?? 'custom'}`, g.kind ?? '');60  const isWorld = g.id === 'world';61  const [countriesRes, extraInd, vsWorld, regions] = await Promise.all([safe(api.countries()), safe(apiExplore.indicators({ featured: true })), isWorld ? Promise.resolve(null) : safe(apiAnalytics.regionsCompare(gslug, 'world')), safe(apiExplore.regions())]);62  const shares = vsWorld?.shares[g.id] ?? vsWorld?.shares[gslug] ?? null;63  const otherDefault = gslug === 'g7' ? 'brics' : 'g7';64  const countries = countriesRes?.items ?? [];65  const memberIds = data.members.map((m) => m.id);66  const aggs = Object.values(data.aggregates);67  const options: IndicatorCard[] = [...data.headline_indicators, ...(extraInd?.items ?? []).filter((i) => EXTRA_RANKING.includes(i.slug) || i.featured)].filter((i) => i.ranking_eligible !== false);68  const bySize = [...data.members].sort((a, b) => (b.values.population?.value ?? 0) - (a.values.population?.value ?? 0));69  const compareSlugs = bySize.slice(0, 8).map((m) => m.slug ?? m.id);70  const compareAll = data.n_members <= 8;7172  return (73    <>74      <PageHeader75        crumbs={[{ href: routes.regions(), label: t('regions.title') }]}76        eyebrow={kindLabel}77        title={name}78        lede={data.description}79        meta={`${t('regions.members', { n: grouped(data.n_members) })}${g.wb_code ? ` · ${g.wb_code}` : ''}`}80        actions={81          compareSlugs.length >= 2 ? (82            <Link href={routes.compare(...compareSlugs)} className={ACTION_CLS}>83              {compareAll ? t('region.compareAll') : t('region.compareFirst', { n: compareSlugs.length })}84            </Link>85          ) : null86        }87      />8889      <Section id="aggregates" title={t('region.aggregates.title')} subtitle={t('region.aggregates.sub')} className="border-t-0">90        {shares ? (91          <dl className="mb-4 grid gap-x-6 min-[361px]:grid-cols-2 md:grid-cols-4">92            {(['gdp_share_pct', 'population_share_pct'] as const).map((k) => {93              const v = shares[k];94              return (95                <div key={k} className="border-t border-rule py-3">96                  <dt className="eyebrow">{k === 'gdp_share_pct' ? t('regions.compare.gdpShare') : t('regions.compare.popShare')}</dt>97                  <dd className="pnum mt-1 text-2xl font-semibold leading-none text-ink">{v != null ? `${fixed(v, 1)} %` : t('common.na')}</dd>98                  <dd className="mt-1.5 h-1.5 w-full max-w-[12rem] overflow-hidden rounded-xs bg-surface-2" aria-hidden>99                    <span className="block h-full bg-accent" style={{ width: `${Math.min(100, v ?? 0)}%` }} />100                  </dd>101                </div>102              );103            })}104          </dl>105        ) : null}106        {aggs.length === 0 ? (107          <p className="text-sm text-ink-3">{t('common.noDataLong')}</p>108        ) : (109          <dl className="grid gap-x-6 min-[361px]:grid-cols-2 md:grid-cols-3 xl:grid-cols-4">110            {aggs.map((a) => (111              <div key={a.indicator.slug} className="border-t border-rule py-3">112                <dt className="text-xs font-medium text-ink-2">113                  <Link href={routes.indicator(a.indicator.slug)} className="link-quiet -my-3 inline-flex min-h-[44px] items-center md:my-0 md:min-h-0">114                    {a.indicator.short_name ?? a.indicator.name}115                  </Link>116                  <span className="ml-1.5 rounded-xs border border-rule px-1 text-2xs uppercase tracking-wide text-ink-3">{tOpt(`regions.${a.kind}`, a.kind)}</span>117                </dt>118                <dd className="pnum mt-1 text-2xl font-semibold leading-none text-ink">{formatValue(a.value, a.indicator)}</dd>119                <dd className="tnum mt-1 text-xs text-ink-3">{t('region.aggregates.n', { n: grouped(a.n ?? 0), year: a.year ?? '' })}</dd>120              </div>121            ))}122          </dl>123        )}124      </Section>125126      <div className="grid gap-x-10 lg:grid-cols-[minmax(0,3fr)_minmax(0,2fr)]">127        <Section id="ranking" title={t('region.ranking.title')} subtitle={t('region.ranking.sub')}>128          <MemberRanking groupSlug={gslug} groupName={name} initial={data.ranking} options={options} />129        </Section>130        <Section id="map" title={t('region.map.title')} subtitle={t('region.map.sub', { n: grouped(data.n_members), name })}>131          {countries.length ? <MemberMap members={memberIds} countries={countries} name={name} /> : <p className="text-sm text-ink-3">{t('common.noDataLong')}</p>}132        </Section>133      </div>134135      {vsWorld && Object.keys(vsWorld.history).length ? (136        <Section id="history" title={t('region.history.title')} subtitle={t('region.history.sub', { name })}>137          <GroupHistory data={vsWorld} ids={[g.id]} names={{ [g.id]: name }} />138        </Section>139      ) : null}140141      <Section id="compare-group" title={t('regions.compare.withOther')} subtitle={t('regions.compare.withOtherSub')} actions={<Link href={routes.regionCompare(gslug, otherDefault)} className="text-accent hover:underline">{t('regions.compare.open')} →</Link>}>142        <GroupComparePicker groups={regions?.items ?? []} a={gslug} b={otherDefault} fixedA />143      </Section>144145      <Section id="members" title={t('region.members.title')} subtitle={t('region.members.sub', { n: grouped(data.n_members) })}>146        {data.members.length ? <MembersTable members={data.members} indicators={data.headline_indicators} /> : <p className="text-sm text-ink-3">{t('common.noDataLong')}</p>}147      </Section>148    </>149  );150}151